<template>
  <basic-container>
    <el-row>
      <el-text>所属产品：</el-text>
      <el-select v-model="productKey" placeholder="请选择产品" filterable clearable @change="productChange"
                 class="margin-node"
                 style="width: 200px">
        <el-option
          v-for="item in productSelect"
          :key="item.productKey"
          :label="item.productName"
          :value="item.productKey">
        </el-option>
      </el-select>
      <el-text class="margin-node">调试设备：</el-text>
      <avue-input-table :props="deviceProps"
                        :children="deviceChildren"
                        :formatter="deviceFormatter"
                        :on-load="onLoadDevice"
                        v-model="deviceId"
                        placeholder="请选择设备" style="width: 200px">
      </avue-input-table>
      <el-text class="margin-node">MQTT地址：</el-text>
      <el-input v-model="mqttUrl" placeholder="请输入MQTT地址" style="width: 300px" />
      <el-button :loading="loading" :disabled="this.isConnected" type="primary" class="margin-node"
                 @click="onConnect">开启模拟
      </el-button>
      <el-button :disabled="!this.isConnected" plain type="danger" class="margin-node"
                 @click="disConnect">关闭模拟
      </el-button>
      <el-button @click="openTsl">物模型 TSL</el-button>
    </el-row>
    <el-row>
      <el-dialog v-model="visibleTsl" :before-close="beforeCloseTsl" title="物模型" width="600">
        <el-row>
          <el-alert
            title="物模型是云端对设备功能的抽象描述，涵盖了设备的属性、服务和事件。物联网平台通过物的描述语言，即 TSL（Thing Specification Language），以 JSON 格式表达这一模型。开发者可以利用 TSL 构建并上报设备数据。完整的物模型可用于云端应用的开发，而精简版的物模型则可结合设备端SDK用于设备的开发工作。"
            type="info" :closable="false" />
        </el-row>
        <el-row>
          <el-select
            v-model="blockId"
            clearable
            placeholder="请选择模块"
            @change="blockTslChange"
            style="width: 240px"
          >
            <el-option
              v-for="item in dataBlock"
              :key="item.id"
              :label="item.isDefault === 1 ? '默认模块': item.blockName"
              :value="item.id"
            />
          </el-select>
        </el-row>
        <el-row>
          <code-editor v-model="tsl" :json="true" :readonly="true" theme="nord" />
        </el-row>
      </el-dialog>
    </el-row>
    <el-row v-if="this.isConnected">
      <el-col :span="8">
        <el-tabs model-value="up" type="border-card">
          <el-tab-pane label="上行指令调试" name="up">
            <el-tabs model-value="propertyUp">
              <el-tab-pane label="属性上报" name="propertyUp">
                <simulator-property v-model="this.dataSnapshot"
                                    @sendCommand="postProperty"></simulator-property>
              </el-tab-pane>
              <el-tab-pane label="事件上报" name="eventUp">
                <simulator-event v-model="this.dataSnapshot"
                                 @sendCommand="postEvent"></simulator-event>
              </el-tab-pane>
              <!--<el-tab-pane label="自定义topic" name="topicUp">

                  </el-tab-pane>-->
            </el-tabs>
          </el-tab-pane>
          <el-tab-pane label="下行指令调试" name="down">
            <el-tabs model-value="propertyDown">
              <el-tab-pane label="属性调试" name="propertyDown">
                <simulator-property ref="propertyDown" v-model="this.dataSnapshot" :down-mode="true"
                                    @sendCommand="sendProperty"></simulator-property>
              </el-tab-pane>
              <el-tab-pane label="命令调用" name="commandDown">
                <simulator-command v-model="this.dataSnapshot"
                                   @sendCommand="postCommand"></simulator-command>
              </el-tab-pane>
              <!--<el-tab-pane label="自定义topic" name="topicDown">

                  </el-tab-pane>-->
            </el-tabs>
          </el-tab-pane>
        </el-tabs>
      </el-col>
      <el-col :span="16">
        <el-tabs model-value="device" type="border-card" class="margin-node">
          <el-tab-pane label="设备日志" name="device">
            <avue-crud :option="option"
                       :table-loading="loading"
                       :data="data"
                       ref="crud">
              <template #message="{ row }">
                <div v-if="row.message.topic">
                  Topic: {{ row.message.topic }}
                  <br />
                  Payload: {{ row.message.payload }}
                </div>
                <div v-else>
                  {{ row.message }}
                </div>
              </template>
            </avue-crud>
          </el-tab-pane>
          <el-tab-pane label="云端日志" name="cloud">
            <device-log ref="deviceLog"
                        :product-key="this.productKey"
                        :device-id="this.deviceId"
                        :device-name="this.deviceName"
                        :simplify-mode="true">
            </device-log>
          </el-tab-pane>
        </el-tabs>
      </el-col>
    </el-row>
    <el-row v-if="!this.isConnected">
      <el-col :span="24">
        <el-result title="设备模拟器"
                   sub-title="设备模拟器可以模拟设备与云端建立 MQTT 连接，上报数据，接收云端控制指令，以及物模型通信等完整过程">
          <template #icon>
            <el-image style="width: 200px; height: 200px" src="/svg/icon1.svg" />
          </template>
          <template #extra>
            <el-row>
              <el-alert type="info" show-icon :closable='false'>
                <template #title>
                  <el-text size="small">设备模拟器仅用于模拟离线设备，若需调试在线设备请移步</el-text>
                  <el-text size="small" type="primary" class="cursor" @click="viewDebugger"> 设备调试</el-text>
                </template>
              </el-alert>
            </el-row>
            <el-row>
              <el-col :span="24">
                <el-button type="primary" @click="onConnect()">启动设备模拟器</el-button>
              </el-col>
            </el-row>
          </template>
        </el-result>
      </el-col>
    </el-row>
  </basic-container>
</template>
<script>
import { select as selectProduct } from '@/api/iot/product';
import { getDetail as getDeviceDetail, getList as getDeviceList, select as selectDevice } from '@/api/iot/device';
import { dataDraftedTsl, dataPublishedByKey, dataPublishedTsl } from '@/api/iot/product/version';
import { validatenull } from '@/utils/validate';
import mqtt from 'mqtt';
import func from '@/utils/func';
import sysTopic from '@/utils/mqtt/sysTopic';
import MqttConfig from '@/utils/mqtt/MqttConfig';
import { mapGetters } from 'vuex';

export default {
  name: 'deviceSimulator',
  data() {
    return {
      mqttUrl: 'ws://localhost:8083',
      productKey: '',
      tsl: '',
      visibleTsl: false,
      reconnectCount: 0,
      productId: '',
      blockId: '',
      dataBlock: [],
      deviceId: '',
      deviceName: '',
      deviceSecret: '',
      deviceProps: {
        label: 'deviceName',
        value: 'id'
      },
      deviceChildren: {
        border: true,
        column: [{
          label: '产品编号',
          prop: 'productKey',
          width: 200
        }, {
          label: '设备名称',
          prop: 'deviceName',
          search: true,
          width: 250
        }, {
          label: '设备密钥',
          prop: 'deviceSecret',
          search: true
        }, {
          label: '唯一编号',
          prop: 'uniqueNo',
          search: true
        }, {
          label: '设备状态',
          prop: 'deviceStatusName',
          width: 120
        }, {
          label: '激活时间',
          prop: 'activeTime',
          width: 180
        }]
      },
      productSelect: {},
      deviceData: {},
      dataSnapshot: {},
      client: null,
      isConnected: false,
      loading: false,
      option: {
        height: 'auto',
        size: 'small',
        calcHeight: 272,
        searchShow: true,
        searchMenuSpan: 6,
        tip: false,
        header: false,
        border: true,
        index: true,
        addBtn: false,
        editBtn: false,
        viewBtn: false,
        menu: false,
        dialogWidth: 500,
        labelWidth: 120,
        searchLabelWidth: 100,
        column: [
          {
            label: '更新时间',
            prop: 'time',
            width: 150
          },
          {
            label: '类型',
            prop: 'type',
            width: 100
          },
          {
            label: '消息内容',
            prop: 'message'
          }
        ]
      },
      data: [],
      localProperty: {}
    };
  },
  computed: {
    ...mapGetters(['userInfo'])
  },
  watch: {
    deviceId() {
      if (!validatenull(this.deviceId)) {
        this.initDevice(this.deviceId);
      }
    }
  },
  created() {
    this.init();
  },
  beforeRouteLeave() {
    this.disConnect();
  },
  methods: {
    init() {
      selectProduct().then(res => {
        this.productSelect = res.data.data;
      });
    },
    initDevice(deviceId) {
      getDeviceDetail(deviceId).then(res => {
        this.deviceData = res.data.data;
        this.deviceName = this.deviceData.deviceName;
        this.deviceSecret = this.deviceData.deviceSecret;
      });
    },
    deviceFormatter(row) {
      if (!row.deviceName) return '';
      return row.deviceName;
    },
    onLoadDevice({ page, value, data }, callback) {
      if (validatenull(this.productKey)) {
        this.$message.warning('请先选择产品');
        callback({
          total: 0,
          data: []
        });
        return;
      }
      let param = {};
      let currentPage = 1;
      let pageSize = 10;
      //首次查询信息
      if (value) {
        getDeviceDetail(value).then(res => {
          callback(res.data.data);
        });
        return;
      }
      if (data) {
        param = data;
      }
      if (page) {
        currentPage = page.currentPage;
        pageSize = page.pageSize;
      }
      //分页查询信息
      getDeviceList(currentPage, pageSize, Object.assign(param, {
        productKey: this.productKey,
        deviceStatus_notequal: 1
      })).then(res => {
        const data = res.data.data;
        callback({
          total: data.total,
          data: data.records
        });
      });
    },
    onConnect() {
      if (validatenull(this.productKey) || validatenull(this.deviceId)) {
        this.$message.warning('请选择产品和设备');
        return;
      }

      this.loading = true;

      // 获取连接参数
      const tenantId = func.getUserTenantId(this.userInfo);
      const config = new MqttConfig(tenantId, this.productKey, this.deviceName, this.deviceSecret, `${this.deviceName}-simulator`);
      const clientId = config.mqttClientId();
      const username = config.username();
      const password = config.password();

      // 创建客户端
      this.client = mqtt.connect(this.mqttUrl, {
        clientId,
        username,
        password
      });

      // 连接成功
      this.client.on('connect', () => {
        this.isConnected = true;
        console.log('mqtt connected');
        this.$message.success('设备连接成功');
        this.loading = false;
        // 加载设备快照数据
        this.loadSnapshot(this.productKey);
        // 订阅主题
        this.onSubscribe();
        // 监听消息
        this.onMessage();
        // 添加连接成功日志
        this.data.unshift({
          time: func.formatDateTime(new Date()),
          type: '连接成功',
          message: `connection is success, productKey=${this.productKey}, deviceName=${this.deviceName}, deviceSecret=${this.deviceSecret}`
        });
      });

      // 无限重连限制次数
      this.client.on('reconnect', () => {
        this.reconnectCount = this.reconnectCount + 1;
        if (this.reconnectCount > 2) {
          this.isConnected = false;
          this.loading = false;
          this.client.end(); // 关闭连接
        }
      });

      // 监听连接错误事件
      this.client.on('error', (error) => {
        this.isConnected = false;
        this.loading = false;
        this.client.end(); // 关闭连接
        this.$message.error('连接失败：' + error);
      });

    },
    onSubscribe() {
      // 云端获取设备属性
      this.client.subscribe(sysTopic.getServicePropertyGet(this.productKey, this.deviceName), function(err) {
        if (!err) {
          console.log('ServicePropertyGet 订阅成功');
        } else {
          console.log('ServicePropertyGet 订阅失败', err);
        }
      });

      // 设备响应云端获取设备属性
      this.client.subscribe(sysTopic.getServicePropertyGetReply(this.productKey, this.deviceName), function(err) {
        if (!err) {
          console.log('ServicePropertyGetReply 订阅成功');
        } else {
          console.log('ServicePropertyGetReply 订阅失败', err);
        }
      });

      // 云端设置设备属性
      this.client.subscribe(sysTopic.getServicePropertySet(this.productKey, this.deviceName), function(err) {
        if (!err) {
          console.log('ServicePropertySet 订阅成功');
        } else {
          console.log('ServicePropertySet 订阅失败', err);
        }
      });

      // 设备响应云端设置设备属性
      this.client.subscribe(sysTopic.getServicePropertySetReply(this.productKey, this.deviceName), function(err) {
        if (!err) {
          console.log('ServicePropertySetReply 订阅成功');
        } else {
          console.log('ServicePropertySetReply 订阅失败', err);
        }
      });

      // 云端下发设备命令
      this.client.subscribe(sysTopic.getServiceCommand(this.productKey, this.deviceName, '+'), function(err) {
        if (!err) {
          console.log('ServiceCommand 订阅成功');
        } else {
          console.log('ServiceCommand 订阅失败', err);
        }
      });

      // 设备响应云端下发设备命令
      this.client.subscribe(sysTopic.getServiceCommandReply(this.productKey, this.deviceName, '+'), function(err) {
        if (!err) {
          console.log('ServiceCommandReply 订阅成功');
        } else {
          console.log('ServiceCommandReply 订阅失败', err);
        }
      });
    },
    onMessage() {
      // 接收订阅消息
      this.client.on('message', (topic, message) => {
        console.log('mqtt message received', topic, message.toString());

        if (topic === sysTopic.getServicePropertyGet(this.productKey, this.deviceName)) {
          // 云端获取设备属性
          this.deviceLocalLog(topic, message, '接收云端下行');
          const data = JSON.parse(message.toString());
          this.getPropertyReply(data);
          return;
        }

        if (topic === sysTopic.getServicePropertySet(this.productKey, this.deviceName)) {
          // 云端设置设备属性
          this.deviceLocalLog(topic, message, '接收云端下行');
          const data = JSON.parse(message.toString());
          if (!validatenull(data.params)) {
            this.$refs.propertyDown.reloadProperty(data.params);
            console.log('设备属性设置成功：' + JSON.stringify(data.params));
            this.setPropertyReply(data);
          }
          return;
        }

        if (topic === sysTopic.getServicePropertyGetReply(this.productKey, this.deviceName)) {
          // 设备响应云端获取设备属性
          const data = JSON.parse(message.toString());
          if (!validatenull(data.params)) {
            this.$refs.propertyDown.reloadProperty(data.params);
            console.log('设备属性获取成功：' + JSON.stringify(data.params));
            //this.$message.success('设备属性获取成功：' + JSON.stringify(data.params));
          }
          return;
        }

        if (topic === sysTopic.getServicePropertySetReply(this.productKey, this.deviceName)) {
          // 设备响应云端设置设备属性
          const data = JSON.parse(message.toString());
          if (!validatenull(data.params)) {
            console.log('设备属性设置成功' + JSON.stringify(data.params));
          }
        }
      });
    },
    disConnect() {
      if (this.isConnected) {
        // 关闭连接
        this.client.end(true, () => {
          this.isConnected = false;
          this.data = [];
          this.$message.success('连接关闭成功');
          console.log('connection closed');
        });
      }
    },
    productChange(value) {
      if (validatenull(value)) {
        return;
      }
      this.productKey = value;
      this.deviceId = '';
    },
    openTsl() {
      if (!this.isConnected) {
        this.$message.warning('请先开启模拟器');
        return;
      }
      // 设置产品ID
      this.productId = this.dataSnapshot.profile.productId;
      // 设置默认模块
      if (validatenull(this.blockId)) {
        this.dataBlock = this.dataSnapshot.blocks;
        const defaultItem = this.dataBlock.find(item => item.isDefault === 1);
        if (!validatenull(defaultItem)) {
          this.blockId = defaultItem.id;
        }
      }
      // 设置物模型
      if (!validatenull(this.productId)) {
        dataPublishedTsl(this.productId, this.blockId).then(res => {
          this.tsl = JSON.stringify(res.data.data);
        });
        this.visibleTsl = true;
      }
    },
    beforeCloseTsl() {
      this.tsl = '';
      this.visibleTsl = false;
    },
    blockTslChange(value) {
      this.blockId = value;
      dataDraftedTsl(this.productId, this.blockId).then(res => {
        this.tsl = JSON.stringify(res.data.data);
      });
    },
    postProperty(data) {
      const req = {
        id: func.generateUUID(),
        version: '1.0',
        params: {
          ...data
        }
      };
      // 更新本地属性
      this.reloadLocalProperty(data);
      //const topic = `/blade/sys/${this.productKey}/${this.deviceName}/thing/event/property/post`;
      const topic = sysTopic.getPropertyPost(this.productKey, this.deviceName);
      const message = JSON.stringify(req);
      this.publish(topic, message);
      this.deviceLocalLog(topic, message, '设备上行');
    },
    postEvent(data, identifier) {
      const req = {
        id: func.generateUUID(),
        version: '1.0',
        params: {
          ...data
        }
      };
      //const topic = `/blade/sys/${this.productKey}/${this.deviceName}/thing/event/${identifier}/post`;
      const topic = sysTopic.getEventPost(this.productKey, this.deviceName, identifier);
      const message = JSON.stringify(req);
      this.publish(topic, message);
      this.deviceLocalLog(topic, message, '设备上行');
    },
    postCommand(data, identifier) {
      const req = {
        id: func.generateUUID(),
        version: '1.0',
        params: {
          ...data
        }
      };
      //const topic = `/blade/sys/${this.productKey}/${this.deviceName}/thing/service/${identifier}`;
      const topic = sysTopic.getServiceCommand(this.productKey, this.deviceName, identifier);
      const message = JSON.stringify(req);
      this.publish(topic, message);
      this.deviceLocalLog(topic, message, '云端下行');
    },
    sendProperty(data, action) {
      let topic = '';
      let params;
      if (action === 'get') {
        topic = sysTopic.getServicePropertyGet(this.productKey, this.deviceName);
        params = [...data];
      } else if (action === 'set') {
        topic = sysTopic.getServicePropertySet(this.productKey, this.deviceName);
        params = { ...data };
      }
      const req = {
        id: func.generateUUID(),
        version: '1.0',
        params: params
      };
      const message = JSON.stringify(req);
      this.publish(topic, message);
      this.deviceLocalLog(topic, message, '云端下行');
    },
    getPropertyReply(data) {
      const id = data.id;
      const item = data.params;
      const params = validatenull(item) ? this.localProperty :
        item.reduce((acc, identifier) => {
          acc[identifier] = this.localProperty[identifier];
          return acc;
        }, {});
      const req = {
        id,
        version: '1.0',
        data: params
      };
      const topic = sysTopic.getServicePropertyGetReply(this.productKey, this.deviceName);
      const message = JSON.stringify(req);
      this.publish(topic, message);
      this.deviceLocalLog(topic, message, '设备上行');
    },
    setPropertyReply(data) {
      const id = data.id;
      const item = data.params;
      const params = validatenull(item) ? this.localProperty : item;
      const req = {
        id,
        version: '1.0',
        data: params
      };
      const topic = sysTopic.getServicePropertySetReply(this.productKey, this.deviceName);
      const message = JSON.stringify(req);
      this.publish(topic, message);
      this.reloadLocalProperty(params);
      this.deviceLocalLog(topic, message, '设备上行');
    },
    publish(topic, message) {
      this.client.publish(topic, message, (err) => {
        if (err) {
          this.$message.error('发送指令失败: ' + err.message);
        } else {
          // 刷新云端日志数据
          this.$refs.deviceLog.onLoad();
        }
      });
    },
    async loadSnapshot(productKey) {
      dataPublishedByKey(productKey).then(res => {
        this.dataSnapshot = res.data.data;
      });
    },
    viewDebugger() {
      this.$router.push({
        path: `/iot/deviceDebugger`
      });
    },
    reloadLocalProperty(data) {
      this.localProperty = {
        ...this.localProperty,
        ...data
      };
    },
    getLocalProperty(identifier) {
      return this.localProperty[identifier];
    },
    deviceLocalLog(topic, message, type) {
      this.data.unshift({
        time: func.formatDateTime(new Date()),
        type: type,
        message: {
          topic: topic,
          payload: JSON.parse(message)
        }
      });
    }
  }
};

</script>
<style>
.margin-node {
  margin-left: 12px;
}
</style>
